/**
 * Calendar control class.
 *
 * @author Dmitry Lipovoi <dmitry.lipovoi@gmail.com>, Andrey Anisimov <hercules@inbox.ru>
 */

import core.ui.Menu;
import core.ui.MenuItem;
import core.ui.ComboBox;
import core.util.Listeners;

// TODO: add return value formatting via setFormat
// TODO: auto detect whether the week should start from sundays or not
class Calendar extends Menu {

	/**
	 * Creates new calendar control.
	 * 
	 * @param parent (Window) [optional] parent window
	 * @param x, y (int) [optional] coordinates
	 * @param width, height (int) [optional] calendar size.
	 */	
	this.Calendar = function(parent, x, y, width, height) {
		super(parent, x, y, width, height);

		this.format = Calendar.FORMAT_DEFAULT;
		this.listeners = new Listeners(['onDateSelected']);

		this.year = Calendar.currentYear;
		this.month = Calendar.currentMonth;
		this.day = Calendar.currentDay;
		
		this.startYear = Calendar.currentYear - 5;
		this.endYear = Calendar.currentYear + 5;

		this.clickHandler = bind(this, this.onItemClick);
		this.mouseoverHandler = bind(this, this.onItemMouseOver);
		this.mouseoutHandler = bind(this, this.onItemMouseOut);

		// Create child controls
		this.createMonthList();
		this.createYearList();
		this.createGrid();
		this.updateData();
	}

	this.addChangeListener = function(listener) {
		this.listeners.add(listener);
	}

	this.removeChangeListener = function(listener) {
		this.listeners.remove(listener);
	}

	/**
	 * Sets the current date to display
	 *
	 * @param year (int) [optional] year number (4 digits).
	 * @param month (int) [optional] month number (from 1 to 12).
	 * @param day (int) day number (from 1 to 31).
	 */
	this.setDate = function(year, month, day) {
		if (year)	this.year = year;
		if (month)	this.month = month;
		if (day)	this.day = day;
		this.updateData();
	}

	/**
	 * Sets a year to display.
	 *
	 * @param year (int) year number (4 digits).
	 */
	this.setYear = function(year) {
		this.year = year;
		this.updateData();
	}
	
	/**
	 * Gets currently selected year.
	 *
	 * @return int
	 */
	this.getYear = function() {
		return this.year;
	}
	
	/**
	 * Sets a month to display.
	 * 
	 * @param month (int) month number (from 1 to 12).
	 */
	this.setMonth = function(month) {
		this.month = month;
		this.updateData();
	}
	
	/**
	 * Returns currently selected month number.
	 *
	 * @return int
	 */
	this.getMonth = function() {
		return this.month;
	}
	
	/**
	 * Sets the day number to display.
	 * 
	 * @param day (int) day number (from 1 to 31).
	 */
	this.setDay = function(day) {
		this.day = day;
		this.updateData();
	}
	
	/**
	 * Returns currently selected day number.
	 *
	 * @return int
	 */
	this.getDay = function() {
		return this.day;
	}

	/**
	 * Sets the start and end years to display in the calendar.
	 * 
	 * @param start (int) [optional] start year (current year - 5 by default)
	 * @param end (int) [opitonal] end year  (current year + 5 by default)
	 */
	this.setYears = function(start, end) {
		if (start)	this.startYear = start;
		if (end)	this.endYear = end;
		this.initYearList();
	}
	
	/**
	 * Gets the start year.
	 *
	 * @return int
	 */
	this.getStartYear = function() {
		return this.startYear;
	}
	
	/**
	 * Gets the end year.
	 *
	 * @return int
	 */
	this.getEndYear = function() {
		return this.endYear;
	}
	
	/**
	 * Sets the date output format.
	 * Can be either predefined like Calendar.FORMAT_TIMESTAMP, Calendar.FORMAT_DEFAULT,
	 * or string constant like 'MM/DD/YYYY', 'MM-DD-YYYY'
	 *
	 * The following conversion specifiers supported:
	 * MM - two digit month number
	 * DD - two digit day number
	 * YYYY - four digit year
	 * 
	 * @param format (string) date format.
	 */
	this.setFormat = function(format) {
		this.format = format;
	}
	
	/**
	 * Gets the date output format.
	 *
	 * @return string
	 */
	this.getFormat = function() {
		return this.format;
	}
	
	/**
	 * Sets the date in specified format (unimplemented).
	 *
	 * @param date (string) date to set
	 * @param format (string) [optional] date format (this.format by default)
	 * @see setFormat
	 */
	this.setFormattedDate = function(date, format) {
		
		if (!format) {
			format = this.format;
		}
		
		if (Calendar.FORMAT_TIMESTAMP == format) {
			
			var timestamp = parseInt(date);
			
			if (isNaN(timestamp)) {
				throw new Error('Invalid `date` parameter ('+ date + ') (should be a timestamp)');
			}
			
			var tmp = new Date();
			tmp.setTime(timestamp * 1000);
			
			this.year = tmp.getFullYear();
			this.month = tmp.getMonth();
			this.day = tmp.getDay();
			
		} else {
			
			// TODO: use core.util.Format.parse
			var map = {
				'MM' : 'month',
				'DD' : 'day',
				'YYYY' : 'year'
			};
			
			for (var key in map) {
				var i = format.indexOf(key);
				if (-1 != i) {
					value = parseInt(date.substr(i, key.length).replace(/^0/, ''));
					if (isNaN(value)) {
						throw new Error('Invalid `date` parameter ('+ date + ')');
					}
					var field = map[key];
					if ('year' == field) {
						this.year = (value > 100) ? value : (2000 + value);
					} else {
						this[field] = value;
					}
				}
			}
		}
		
		this.updateData();
	}

	/**
	 * Returns date in speficied format.
	 * 
	 * @param format (string) [optional] date format (this.format by default)
	 * @return string
	 * @see setFormat
	 */
	this.getFormattedDate = function(format) {
		
		if (!format) {
			format = this.format;
		}

		if (Calendar.FORMAT_TIMESTAMP == format) {
			return this.getTimestamp();
		}
		
		var year = this.year.toString();
		var month = (this.month < 10) ? '0' + this.month : this.month.toString();
		var day = (this.day < 10) ? '0' + this.day : this.day.toString();

		// TODO: add more format specifiers
		// TODO: use core.util.Format.string()
		var mapping = {
			'MM' : month,
			'DD' : day,
			'YYYY' : year
		};
		
		for (var pattern in mapping) {
			var re = new RegExp(pattern);
			var value = mapping[pattern];
			format = format.replace(re, value);
		}
		
		return format;
	}
	
	/**
	 * Returns timestamp value for the selected date.
	 *
	 * Note:
	 * If timeZone paramenter is specified, the returned value
	 * will be timestamp in the specified time zone.
	 * Otherwise, the returned timestamp will be in GMT time.
	 *
	 * @param timeZone (int) [optional] local timezone.
	 * @return long
	 */
	this.getTimestamp = function(timeZone) {
		return timeZone
			? new Date(Date.UTC(this.year, this.month - 1, this.day, -timeZone)).getTime()/1000
			: new Date(Date.UTC(this.year, this.month - 1, this.day)).getTime()/1000;
	}

	/**
	 * Returns the child combobox control.
	 *
	 * @return ComboBox
	 */
	this.getYearList = function() {
		return this.yearList;
	}
	
	/**
	 * Returns the child combobox control.
	 *
	 * @return ComboBox
	 */
	this.getMonthList = function() {
		return this.monthList;
	}
	
	/**
	 * @protected
	 * Updates calendar data.
	 */
	this.updateData = function() {
		
		var leapYear = ((this.year % 4 == 0) && (this.year % 100 != 0)) || (this.year % 400 == 0);
		var daysInMonth = [ 31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
		var offset = new Date(this.year, this.month - 1).getDay();

		if (this.year < this.startYear) this.year = this.startYear;
		else if (this.year > this.endYear) this.year = this.endYear;
		
		var prevMonth, prevYear; 
		if (this.month > 1) {
			prevMonth = this.month - 1;
			prevYear = this.year;
		} else {
			prevMonth = 12;
			prevYear = this.year - 1;
		}
		
		var nextMonth, nextYear;
		if (this.month < 12) {
			nextMonth = this.month + 1;
			nextYear = this.year;
		} else {
			nextMonth = 1;
			nextYear = this.year + 1;
		}
		
		for (var i = 0, start = daysInMonth[prevMonth - 1] - offset + 1; i < offset; i++) {
			// Previous month items
			this.items[i].month = prevMonth;
			this.items[i].year = prevYear;
			this.items[i].day = start + i;
		}

		for (var i = offset; i < daysInMonth[this.month - 1] + offset; i++) {
			// Current month items
			this.items[i].year = this.year;
			this.items[i].month = this.month;
			this.items[i].day = i - offset + 1;
		}
		
		for (var i = daysInMonth[this.month - 1] + offset; i < 42; i++) {
			// Next month items
			this.items[i].year = nextYear;
			this.items[i].month = nextMonth;
			this.items[i].day = i - (daysInMonth[this.month - 1] + offset) + 1;
		}
		
		this.updateControls();
	}
	
	/**
	 * @protected
	 * Updates child controls.
	 */
	this.updateControls = function() {
		
		this.yearList.setValue(this.year);
		this.monthList.setValue(this.month);
		
		for (var i = 0; i < this.items.length; i++) {
			var item = this.items[i];
			
			// Change menu item style
			item.clearStyleName();
			
			// Selected item
			if ((item.year == this.year) &&
				(item.month == this.month) &&
				(item.day == this.day))
			{
				item.addStyleName('CalendarMenuItem-selected');
			}
			
			// Today's item
			if ((item.year == Calendar.currentYear) &&
				(item.month == Calendar.currentMonth) &&
				(item.day == Calendar.currentDay))
			{
				item.addStyleName('CalendarMenuItem-today');
			}
			
			// Previous or next month's item
			if ((item.year != this.year) || (item.month != this.month)) {
				item.addStyleName('CalendarMenuItem-grayed');
			}

			// HARDCODE: if sunday or saturday
			// TODO: auto detect whether the week should start from sundays or not
			if (i % 7 == 0 || i % 7 == 6) {
				item.addStyleName('CalendarMenuItem-holiday');
			}

			item.setText(item.day);
		}
	}

	/**
	 * @protected
	 * Creates dropdown list of month names.
	 */
	this.createMonthList = function() {
		this.monthList = new ComboBox(this);
		this.monthList.addStyleName('CalendarMonthList');
		this.monthList.setStyle('float', 'left');
		this.monthList.addChangeListener(this);
		
		var items = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July',
			'August', 'September', 'October', 'November', 'December' ];
			
		for (var i = 0; i < items.length; i++) {
			this.monthList.addItem(items[i], i + 1, (i == this.month - 1));
		}
	}

	/**
	 * @protected
	 * Creates dropdown list of years.
	 */
	this.createYearList = function() {
		this.yearList = new ComboBox(this);
		this.yearList.addStyleName('CalendarYearList');
		this.yearList.setStyle('float', 'right');
		this.yearList.addChangeListener(this);
		this.initYearList();
	}

	/**
	 * @protected
	 * Fill yearList with years in range from startYear to endYear.
	 */
	this.initYearList = function() {
		this.yearList.removeItems();
		for (var i = this.startYear; i <= this.endYear; i++) {
			this.yearList.addItem(i.toString(), i, (i == this.year));
		}
	}
	
	/**
	 * @protected
	 * Creates calendar grid.
	 */
	this.createGrid = function() {
		// Create weekdays header
		var days = [ 'Su', 'Mo', 'Tu', 'W', 'Th', 'Fr', 'Sa' ];
		for (var i = 0; i < days.length; i++) {
			var item = new MenuItem(this);
			item.setStyleName('CalendarHeaderItem');
			item.setStyle('float', 'left');
			item.setText(days[i]);
		}
		// Create calendar grid
		for (var i = 0; i < 42; i++) {
			var item = new MenuItem(this);
			item.setStyleName('CalendarMenuItem');
			item.setStyle('float', 'left');
			item.onclick = this.clickHandler;
			item.onmouseover = this.mouseoverHandler;
			item.onmouseout = this.mouseoutHandler;
			this.addItem(item);
		}
	}

	/**
	 * @protected
	 */
	this.fireDateSelected = function() {
		this.listeners.call('onDateSelected', [this]);
	}

	this.onValueChanged = function(sender) {
		if (this.yearList == sender) {
			this.year = sender.getValue();
		} else if (this.monthList == sender) {
			this.month = sender.getValue();
		}
		this.day = -1;
		this.updateData();
	}

	this.onItemClick = function(event, item) {
		this.day = item.day;
		if ((this.month != item.month) || (this.year != item.year)) {
			this.month = item.month;
			this.year = item.year;
			this.updateData();
		} else {
			this.updateControls();
		}
		this.fireDateSelected();
	}

	this.onItemMouseOver = function(event, item) {
		item.addStyleName('CalendarMenuItem-highlight');
	}

	this.onItemMouseOut = function(event, item) {
		item.removeStyleName('CalendarMenuItem-highlight');
	}
}

/**
 * Current year (4-digit number).
 * @type int
 */
core.ui.Calendar.currentYear = new Date().getFullYear();

/**
 * Current month number (from 1 to 12).
 * @type int
 */
core.ui.Calendar.currentMonth = new Date().getMonth() + 1;

/**
 * Current day number (from 1 to 31).
 * @type int
 */
core.ui.Calendar.currentDay = new Date().getDate();

core.ui.Calendar.FORMAT_TIMESTAMP = 'TIMESTAMP';
core.ui.Calendar.FORMAT_DEFAULT = 'MM/DD/YYYY';
